package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.SystemConstants;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static com.hmdp.utils.RedisConstants.SHOP_FEO_KEY;
import static com.hmdp.utils.RedisConstants.SHOP_GEO_KEY;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 可重入锁用于控制重试逻辑
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public Result queryById(Long id) {
        // 解决缓存穿透
        //Shop shop = handleCachePenetration(id);
        // 调用缓存击穿处理方法
        Shop shop = handleCacheBreakdown(id);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 根据类型分页分类查询店铺信息
     * @param typeId
     * @param current
     * @param x
     * @param y
     * @return
     */
    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.判断 是否需要根据坐标查询
        if(x==null || y == null){
            // 不需要坐标查，按数据库查
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }

        //2. 计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;

        //3. 查询redis，按照类型、页码、距离升序：结果：shopId 、 distance
        String key = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(
                        key,
                        GeoReference.fromCoordinate(x,y),
                        new Distance(SHOP_FEO_KEY),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                                .includeDistance()
                                .limit(end)
                );

        //4. 解析出id
        if(results == null){
            return Result.ok(Collections.emptyList());
        }
        // 解析
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();

        if(list.size() <= from){
            return Result.ok(Collections.emptyList());
        }

        //4.1 截取 from 到 end 的部分
//        list.subList(from,end);
        // 定义一个集合，保存店铺id和距离的关系
        List<Long> ids = new ArrayList<>(list.size());
        Map<String,Distance> distanceMap = new HashMap<>(list.size());

        list.stream().skip(from).forEach(result -> {
            // 4.2 获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            // 4.3 获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance);

        });
        //5. 查询店铺
        List<Shop> shops = query().in("id",ids)
                .last("ORDER BY FIELD(id," + StrUtil.join(",",ids) + ")")
                .list();

        // 匹配店铺距离
        for(Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }

        //6. 返回结果
        return Result.ok(shops);
    }

    // 解决缓存穿透
    public Shop handleCachePenetration(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        // 1.查询缓存店铺信息
        String shopCache = stringRedisTemplate.opsForValue().get(key);
        // 1.2 如果存在，则直接返回
        if (StrUtil.isNotBlank(shopCache)) {
            return JSONUtil.toBean(shopCache, Shop.class);
        }
        // 判断命中的缓存是否为空值
        if (shopCache != null) {
            return null;
        }
        // 2.不存在，则查询数据库
        Shop shop = baseMapper.selectById(id);
        // 3.判断店铺信息是否存在
        if (shop == null) {
            // 存入空值，设置短期过期（如5分钟），防止缓存穿透
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 4.存在，则写入缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return shop;
    }


    // 优化后的缓存击穿和缓存穿透处理
    public Shop handleCacheBreakdown(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;

        // 1.先查询缓存
        String shopCache = stringRedisTemplate.opsForValue().get(key);
        // 1.1 缓存存在且不为空，直接返回
        if (StrUtil.isNotBlank(shopCache)) {
            return JSONUtil.toBean(shopCache, Shop.class);
        }
        // 1.2 缓存命中空值，直接返回null（处理缓存穿透场景）
        if ("".equals(shopCache)) {
            return null;
        }

        Shop shop = null;
        try {
            // 2.尝试获取分布式锁
            boolean isLocked = handleCacheBreakdownWithMutex(id);
            // 2.1 如果获取锁失败，说明有其他线程正在加载数据，休眠后重试
            if (!isLocked) {
                // 短暂休眠，避免CPU空转，减轻服务器压力
                Thread.sleep(50);
                // 递归重试，直到获取到数据或确定数据不存在
                return handleCacheBreakdown(id);
            }

            // 3.二次校验缓存，防止多线程竞争时重复查询数据库
            shopCache = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopCache)) {
                return JSONUtil.toBean(shopCache, Shop.class);
            }
            if (shopCache != null) {
                return null;
            }

            // 4.获取锁成功，查询数据库
            shop = baseMapper.selectById(id);

            // 5.模拟数据库查询耗时，放大并发问题（实际生产环境可删除）
            //Thread.sleep(200);

            // 6.判断店铺是否存在
            if (shop == null) {
                // 存入空值，防止缓存穿透
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            // 7.将数据写入缓存
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
                    RedisConstants.CACHE_SHOP_TTL + (int)(Math.random() * 10), // 随机过期时间，避免缓存雪崩
                    TimeUnit.MINUTES);

        } catch (InterruptedException e) {
            throw new RuntimeException("查询店铺信息失败", e);
        } finally {
            // 8.无论结果如何，释放锁
            unlock(id);
        }

        // 9.返回结果
        return shop;
    }

    // 建立互斥锁（分布式锁）
    public Boolean handleCacheBreakdownWithMutex(Long id) {
        String key = RedisConstants.LOCK_SHOP_KEY + id;
        // 使用setIfAbsent实现分布式锁，设置过期时间防止死锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1",
                RedisConstants.LOCK_SHOP_TTL,
                TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    // 释放锁
    public void unlock(Long id) {
        String key = RedisConstants.LOCK_SHOP_KEY + id;
        stringRedisTemplate.delete(key);
    }
}
